Pressidian
花园入口
笔记
项目
关于
实验室
GitHub
花园入口
笔记
项目
关于
实验室
GitHub

KNOWLEDGE PATHS

笔记库
当前位置
笔记库/前端/三件套/JS/JavaScript

06 集合与迭代

22 分钟阅读 · Note

目录树 578 篇

            • 00 JavaScript学习地图
            • 01 JavaScript基础
            • 02 类型系统与类型转换
            • 03 Number与String
            • 04 Array数组
            • 05 Object对象
            • 06 集合与迭代
            • 07 函数、作用域与this
            • 08 原型、构造函数与类
            • 09 Generator与异步迭代
            • 10 异步编程
            • 11 浏览器与DOM
            • 12 二进制与文件
            • 13 标准内建对象
            • 14 模块化
            • 15 错误处理
            • 16 JSDoc
            • 90 JavaScript知识点总目录
            • 91 JavaScript修正与补充记录
          • 前端模块化
      • 前端技术栈
    • 笔记目录
    • CLAUDE.md
    • Vue 组件与 Render 函数

关联笔记 6

↗00 JavaScript学习地图反向链接↗90 JavaScript知识点总目录反向链接↗91 JavaScript修正与补充记录反向链接↗01 JavaScript基础同一路径↗02 类型系统与类型转换同一路径↗03 Number与String同一路径
  • 06 集合与迭代

06 集合与迭代

> Last Format Time:8/11/2026 21:45:38

本笔记由两个 JavaScript 教程目录中的同主题内容合并而成;全部旧知识保留在“旧笔记知识全集(按来源保留)”中,并由导航逐项索引。


当前前端开发关键要点

> [!important] 学习优先级:P1 > - 数组等可迭代对象使用 for...of;普通对象键枚举使用 Object.keys 等 API,谨慎使用 for...in。 > - 需要任意类型键时使用 Map,需要唯一值集合时使用 Set;不要只为语法新颖而替换简单对象或数组。 > - 理解 iterable、iterator、类数组与展开语法的关系,能判断一个值为何可以被遍历。 > - 了解 WeakMap、WeakSet 的弱引用特点及其不可枚举限制。


知识点导航

[现代JS教程与阮一峰ES6 · 1数据类型与变量/Object/for...in与for...of](./06 集合与迭代.md)

  • JS中 for...in 和 for...of 的核心区别(超清晰版)
  • 一句话总结
  • 遍历的内容不同(最核心区别)
  • for...in
  • for...of
  • 适用数据类型不同
  • for...in
  • for...of
  • 遍历数组时的表现差异(重点)
  • 支持的语法特性
  • 最佳使用场景(背会就不会用错)
  • ✅ 用 for...in 的场景
  • ✅ 用 for...of 的场景
  • 极简记忆口诀
  • 总结

[现代JS教程与阮一峰ES6 · 1数据类型与变量/Object/可迭代对象](./06 集合与迭代.md)

  • 可迭代对象

[现代JS教程与阮一峰ES6 · 1数据类型与变量/Object/类数组](./06 集合与迭代.md)

  • 类数组

[现代JS教程与阮一峰ES6 · 5特殊内建对象/Map和Set](./06 集合与迭代.md)

  • JavaScript中Map的实现原理
  • 核心实现:哈希表
  • 关键技术点与优化
  • 哈希冲突处理
  • 键的多样性支持
  • 迭代顺序维护
  • 总结:实现效果的原因
  • 与Object的对比
  • Map Set 映射和集合
  • Map
  • Set
  • 弱映射和弱集合
  • Set 的基本特性
  • Set 的常用方法与属性
  • 基本操作
  • 示例:
  • 遍历方法
  • 示例:
  • Set 的典型应用场景
  • 与数组的区别
  • 总结

[廖雪峰教程 · 3快速入门/3.8 Map与Set](./06 集合与迭代.md)

  • Map与Set
  • Map
  • Set

[廖雪峰教程 · 3快速入门/3.9 iterable](./06 集合与迭代.md)

  • iterable

现代前端补充与纠错

> [!info] 修改标记 > - 【修正】:旧教程中错误、过时或容易误导的内容。 > - 【补充】:旧教程未覆盖、但当前前端开发需要掌握的内容。 > - 【修正代码】:替换或校正了旧代码示例。

> [!warning] 下方保留旧教程上下文;迭代器代码已直接修正,集合复杂度和弱集合规则已就地标成 【修正】。

正确的可迭代对象

【修正】 return 后不能换行再写对象,否则自动分号插入会返回 undefined;循环变量也应显式声明:

【修正代码】

const range = {
  from: 1,
  to: 3,
  [Symbol.iterator]() {
    let current = this.from;
    const last = this.to;
    return {
      next() {
        return current <= last
          ? { value: current++, done: false }
          : { value: undefined, done: true };
      },
    };
  },
};

for (const item of range) {
  console.log(item);
}

Map、Set 与弱集合

  • 【补充】 规范只要求 Map 的平均访问时间优于线性,不保证具体实现一定是哈希表或严格 O(1);复杂度结论应结合引擎和数据规模。
  • 【补充】 Map 适合任意键和频繁增删;普通对象更适合固定字段记录。不要仅凭“性能更快”做选择。
  • 【修正】 当前规范中 WeakMap/WeakSet 的弱持有键可为对象或未注册的 Symbol;字符串、数字等原始值仍不可用。
  • 【补充】 弱集合不可枚举是可达性与垃圾回收语义的一部分,不是功能缺失。

旧笔记知识全集(按来源保留)

现代JS教程与阮一峰ES6 · 1数据类型与变量/Object/可迭代对象

可迭代对象

==实现了 [Symbol.iterator] 属性==的对象即是 Iterable 对象,然后可以使用操作符 for...of 进行迭代

为了让对象可迭代(也就让 for..of 可以运行)我们需要为对象添加一个名为 Symbol.iterator 的方法(一个专门用于使对象可迭代的内建 symbol)。

  1. 当 for..of 循环启动时,它会调用这个方法(如果没找到,就会报错)。这个方法必须返回一个 迭代器(iterator) —— 一个有 next 方法的对象。
  2. 从此开始,for..of 仅适用于这个被返回的对象。
  3. 当 for..of 循环希望取得下一个数值,它就调用这个对象的 next() 方法。
  4. next() 方法返回的结果的==格式必须是 {done: Boolean, value: any}==,当 done=true 时,表示循环结束,否则 value 是下一个值。
let data = {
    name: 'dano',
    age: 21,
    eat: true,
    from: 1,
    to: 5,
}

data[Symbol.iterator] = function () {
    return {
        name: this.name,
        age: this.age,
        current: this.from,
        last: this.to,

        next() {
            if (this.current <= this.last)
            {
                return {
                    done: false,
                    value: this.current++,
                }
            }
            else
            {
				return {
					done: true,
				}
			}
        },
    }
}

for (const item of data) {
    console.log(item);
}

8/11/26 【修正代码】原内容:return 后换行再写对象,且 item 未声明;修改点:避免 ASI 返回 undefined,并改为 for (const item of data)。

上面代码中,变量arr是一个数组,原生就具有遍历器接口,部署在arr的Symbol.iterator属性上面。所以,调用这个属性,就得到遍历器对象。

对于原生部署 Iterator 接口的数据结构,不用自己写遍历器生成函数,for...of循环会自动遍历它们。除此之外,其他数据结构(主要是对象)的 Iterator 接口,都需要自己在Symbol.iterator属性上面部署,这样才会被for...of循环遍历。

yield*后面跟的是一个可遍历的结构,它会调用该结构的遍历器接口。

let generator = function* () {
  yield 1;
  yield* [2,3,4];
  yield 5;
};

var iterator = generator();
iterator.next() // { value: 1, done: false }
iterator.next() // { value: 2, done: false }
iterator.next() // { value: 3, done: false }
iterator.next() // { value: 4, done: false }
iterator.next() // { value: 5, done: false }
iterator.next() // { value: undefined, done: true }

字符串是一个类似数组的对象,也原生具有 Iterator 接口。

var someString = "hi";
typeof someString[Symbol.iterator]
// "function"

var iterator = someString[Symbol.iterator]();

iterator.next()  // { value: "h", done: false }
iterator.next()  // { value: "i", done: false }
iterator.next()  // { value: undefined, done: true }

Symbol.iterator()方法的最简单实现,还是使用下一章要介绍的 Generator 函数。

let myIterable = {
  [Symbol.iterator]: function* () {
    yield 1;
    yield 2;
    yield 3;
  }
};
[...myIterable] // [1, 2, 3]

// 或者采用下面的简洁写法

let obj = {
  * [Symbol.iterator]() {
    yield 'hello';
    yield 'world';
  }
};

for (let x of obj) {
  console.log(x);
}
// "hello"
// "world"

JavaScript 原有的for...in循环,只能获得对象的键名,不能直接获取键值。ES6 提供for...of循环,允许遍历获得键值。

var arr = ['a', 'b', 'c', 'd'];

for (let a in arr) {
  console.log(a); // 0 1 2 3
}

for (let a of arr) {
  console.log(a); // a b c d
}

Set 和 Map 结构也原生具有 Iterator 接口,可以直接使用for...of循环。

var engines = new Set(["Gecko", "Trident", "Webkit", "Webkit"]);
for (var e of engines) {
  console.log(e);
}
// Gecko
// Trident
// Webkit

var es6 = new Map();
es6.set("edition", 6);
es6.set("committee", "TC39");
es6.set("standard", "ECMA-262");
for (var [name, value] of es6) {
  console.log(name + ": " + value);
}
// edition: 6
// committee: TC39
// standard: ECMA-262

上面代码演示了如何遍历 Set 结构和 Map 结构。值得注意的地方有两个,首先,遍历的顺序是按照各个成员被添加进数据结构的顺序。其次,Set 结构遍历时,返回的是一个值,而 Map 结构遍历时,返回的是一个数组,该数组的两个成员分别为当前 Map 成员的键名和键值。

let map = new Map().set('a', 1).set('b', 2);
for (let pair of map) {
  console.log(pair);
}
// ['a', 1]
// ['b', 2]

for (let [key, value] of map) {
  console.log(key + ' : ' + value);
}
// a : 1
// b : 2

数组、Set、Map 都部署了以下三个方法,调用后都返回遍历器对象。

  • entries() 返回一个遍历器对象,用来遍历[键名, 键值]组成的数组。对于数组,键名就是索引值;对于 Set,键名与键值相同。Map 结构的 Iterator 接口,默认就是调用entries方法。
  • keys() 返回一个遍历器对象,用来遍历所有的键名。
  • values() 返回一个遍历器对象,用来遍历所有的键值。

这三个方法调用后生成的遍历器对象,所遍历的都是计算生成的数据结构。

let arr = ['a', 'b', 'c'];
for (let pair of arr.entries()) {
  console.log(pair);
}
// [0, 'a']
// [1, 'b']
// [2, 'c']

现代JS教程与阮一峰ES6 · 1数据类型与变量/Object/类数组

类数组

在 JavaScript 中,类数组(Array-like) 或类数组对象(Array-like Object) 是指那些==外观和行为类似数组==,但本质上不是 Array 实例的对象。

它们的核心特征是:

  • 具有 ==length 属性==(表示元素数量);
  • 可以通过==索引(如 0、1、2)访问元素==;
  • 但==不具备 Array 原型==方法(如 push、map、forEach 等)。

常见的类数组对象

  1. arguments 对象 函数内部的局部变量,包含函数调用时传递的所有参数。
   function sum() {
     console.log(arguments); // [1, 2, 3](类数组形式)
     console.log(arguments.length); // 3
     console.log(arguments[0]); // 1(通过索引访问)
   }
   sum(1, 2, 3);
  1. DOM 集合 如 document.getElementsByTagName()、document.querySelectorAll() 返回的集合(HTMLCollection 或 NodeList)。
   const divs = document.getElementsByTagName('div');
   console.log(divs.length); // 页面中 div 的数量
   console.log(divs[0]); // 第一个 div 元素
  1. 字符串(String) 字符串可以通过索引访问字符,且有 length 属性,也被视为类数组。
   const str = "hello";
   console.log(str.length); // 5
   console.log(str[0]); // "h"
  1. 自定义类数组对象 手动定义的具有 length 和索引属性的对象。
   const arrLike = {
     0: "a",
     1: "b",
     2: "c",
     length: 3
   };

类数组与数组的区别

特性类数组对象数组(Array)
原型链原型通常是 Object.prototype原型是 Array.prototype
数组方法不具备(如 map、forEach 等)具备所有数组方法
instanceof Array返回 false返回 true

类数组转换为真正的数组 由于类数组不支持数组方法,实际开发中常需要将其转换为真正的数组,以便使用 map、filter 等方法。常用转换方式:

  1. Array.from()(推荐,ES6+) 专门用于将类数组或可迭代对象转换为数组。
   const arrLike = { 0: 1, 1: 2, 2: 3, length: 3 };
   const arr = Array.from(arrLike);
   console.log(arr); // [1, 2, 3]
   console.log(arr.map(x => x * 2)); // [2, 4, 6](可使用数组方法)
  1. 扩展运算符(...)(ES6+,==仅适用于可迭代对象==) 如 NodeList、字符串等可迭代的类数组。
   const divs = document.querySelectorAll('div');
   const divArray = [...divs]; // 转换为数组

   const str = "hello";
   const strArray = [...str]; // ["h", "e", "l", "l", "o"]
  1. Array.prototype.slice.call()(传统方法) 利用 slice 方法的特性转换(slice 不传参数时返回原数组的副本)。
   const argumentsArr = Array.prototype.slice.call(arguments);
   // 等价于:[].slice.call(arguments)

总结

  • 类数组对象是“长得像数组”的对象,核心特征是 length 属性和索引访问;
  • 常见于 arguments、DOM 集合、字符串等;
  • 转换为真正的数组后,才能使用 Array 的所有方法,常用 Array.from() 或扩展运算符实现转换。

理解类数组的特性有助于在处理 DOM 操作、函数参数等场景时避免错误(如直接调用数组方法导致的报错)。

有==索引和 length 属性 ==、=> 类数组对象

let arrayLike = { // 有索引和 length 属性 => 类数组对象
  0: "Hello",
  1: "World",
  length: 2
};

// Error (no Symbol.iterator)
for (let item of arrayLike) {}

有一个全局方法 Array.from 可以接受一个==可迭代或类数组==的值,并从中获取一个“真正的”数组。然后我们就可以对其调用数组方法了。

现代JS教程与阮一峰ES6 · 1数据类型与变量/Object/for...in与for...of

JS中 for...in 和 for...of 的核心区别(超清晰版)

这是 JavaScript 面试和日常开发高频考点,我用最简单、最实用的方式给你讲清楚,一看就懂。

一句话总结
  • for...in:遍历键名(key/index),适合遍历对象,不适合遍历数组
  • for...of:遍历值(value),适合遍历数组/类数组/可迭代对象,ES6 新增
遍历的内容不同(最核心区别)
for...in

遍历键名(对象的 key、数组的 index)

const arr = ['a', 'b', 'c'];
for (let index in arr) {
  console.log(index); // 输出 0, 1, 2 (字符串类型)
}
for...of

遍历值(数组的元素、迭代器返回的值)

const arr = ['a', 'b', 'c'];
for (let value of arr) {
  console.log(value); // 输出 a, b, c
}
适用数据类型不同
for...in

可以遍历对象(普通对象、数组都能用)

const obj = { name: '小明', age: 18 };
for (let key in obj) {
  console.log(key, obj[key]); // name 小明,age 18
}

⚠️ 缺点:会遍历出原型链上的属性,容易出问题。

for...of

不能直接遍历普通对象,只能遍历可迭代对象:

  • 数组
  • 字符串
  • Map / Set
  • arguments、DOM 集合
  • Generator 等
// 遍历字符串
for (let char of 'hello') {
  console.log(char); // h e l l o
}
遍历数组时的表现差异(重点)
特性for...infor...of
遍历内容数组下标(字符串类型)数组元素
顺序不保证顺序严格按数组顺序
原型属性会遍历出来不会
数组新增方法会遍历到不会

例子:for...in 会踩坑

Array.prototype.test = '123';
const arr = [10, 20];

for (let index in arr) {
  console.log(index);
  // 输出 0、1、test ❌ 把原型上的属性也遍历了
}

for (let value of arr) {
  console.log(value);
  // 输出 10、20 ✅ 安全、干净
}
支持的语法特性
  • for...in:不支持 await(异步遍历)
  • for...of:支持 await(非常实用)
// 异步遍历(for...of 独有)
async function test() {
  const arr = [1, 2, 3];
  for (let item of arr) {
    await new Promise(r => setTimeout(r, 1000));
    console.log(item);
  }
}
最佳使用场景(背会就不会用错)
✅ 用 for...in 的场景
  • 遍历普通对象({})获取键值对
✅ 用 for...of 的场景
  • 遍历数组
  • 遍历字符串
  • 遍历 Map / Set
  • 异步遍历
  • 需要直接拿元素值的时候
极简记忆口诀

> 看键用 in,看值用 of;对象用 in,数组用 of。

总结
  1. for...in 遍历键/下标,适合对象,会遍历原型属性,不推荐遍历数组
  2. for...of 遍历值/元素,适合数组/字符串/可迭代对象,安全、现代、推荐
  3. 遍历数组优先用 for...of,遍历普通对象用 for...in

现代JS教程与阮一峰ES6 · 5特殊内建对象/Map和Set

JavaScript中Map的实现原理
核心实现:哈希表

8/11/26 【修正】原内容:JavaScript Map 的底层必然是哈希表并保证平均 O(1);修改点:规范只要求平均访问时间优于线性,具体哈希表、树或混合结构属于引擎实现。以下段落仅作为一种可能实现理解。 哈希表是实现 Map 理想的数据结构,因为它能提供非常高效的查找、插入和删除操作,平均时间复杂度接近 O(1)。

为什么哈希表能实现这个效果? 它的核心思想是使用一个哈希函数 (Hash Function) 将一个键 (key) 转换成一个数字(哈希值),然后将这个数字映射到内部数组的一个索引上。数据就存储在这个数组对应的位置。

基本工作流程

  1. 设置值 (set(key, value)):
    • 对键 key 应用哈希函数,计算出一个哈希值。
    • 将这个哈希值通过某种运算(比如取模 %)转换到内部数组的一个索引 index。
    • 将键值对存储到数组的 index 位置。
  2. 获取值 (get(key)):
    • 对同一个键 key 再次应用相同的哈希函数,得到相同的哈希值。
    • 计算出相同的索引 index。
    • 直接去数组的 index 位置读取数据。

正因为通过索引直接访问数组元素是极快的,所以 Map 的操作才如此高效。

关键技术点与优化

单纯的哈希表会遇到一些问题,JS 引擎的实现中会使用高级技术来解决它们。

哈希冲突处理

当两个不同的键经过哈希函数计算后得到了相同的数组索引,就发生了哈希冲突。Map 必须有能力处理这种情况。

解决方案

最常见的解决方案是链地址法 (Separate Chaining)。数组的每一个位置不再直接存储一个键值对,而是存储一个链表(或数组)。当发生冲突时,新的键值对会被添加到这个索引对应的链表中。

  • 查找时:先通过索引找到链表,然后再遍历这个短链表来找到准确的键(使用严格相等 === 进行比较)。
  • 优化:为了保持高效,引擎会监控哈希表的"负载因子"(已用位置/总位置)。当负载因子过高时,会执行"扩容"(Rehashing),即创建一个更大的新数组,并重新计算所有键的哈希值和新索引,然后将数据迁移过去。这虽然是一次昂贵的操作,但平均摊还下来,依然能保持 O(1) 的时间复杂度。
键的多样性支持

与普通对象(Object)只能使用字符串或 Symbol 作为键不同,Map 的键可以是任何类型,包括对象、函数、数字、字符串等。

实现方式

引擎会为每个键(尤其是对象这样的引用类型)生成一个唯一的标识符或内部哈希码,用于计算哈希值。这个细节对 JavaScript 开发者是完全透明的。

迭代顺序维护

Map 的一个关键特性是它保持键的插入顺序。当你用 for...of 循环遍历 Map 时,得到的顺序与你插入 set 的顺序一致。

实现方式

单纯的哈希表本身不记录插入顺序。为了实现这个特性,引擎在哈希表之外,还会维护一个双向链表(或类似的有序数据结构)来记录键值对的插入顺序。

  • 当你插入一个新的键值对时,它既会被放入哈希表中,也会被追加到链表的末尾。
  • 当你遍历 Map 时,引擎实际上是在遍历这个链表,从而保证了顺序。
  • 删除操作也需要同时从哈希表和链表中移除该项。

这是一种典型的空间换时间的策略,用额外的内存开销来换取确定的迭代顺序。

总结:实现效果的原因

Map 能实现哈希表的效果是因为:

  1. 基于哈希函数: 使用哈希函数将键快速映射到内存地址,实现了平均 O(1) 的访问速度。
  2. 处理冲突: 使用链地址法等技术妥善处理哈希冲突,保证数据的正确性。
  3. 动态扩容: 在数据量增加时自动扩容,维持高效的性能。
  4. 维护顺序: 通过额外的数据结构(如链表)来维护插入顺序,满足了 ECMAScript 规范的要求。
  5. 引擎优化: 像 V8 这样的引擎还会根据 Map 的大小和操作类型进行多种优化(例如,对于小 Map 可能使用不同的底层表示),但这些实现细节对开发者是隐藏的。
与Object的对比
特性MapObject
键类型任意值String 或 Symbol
迭代顺序可靠的插入顺序现代规范定义了属性顺序,但整数索引、字符串键和 Symbol 键分组规则不同
大小获取size 属性(O(1))手动计算(O(n))
性能在频繁增删键值对的场景下优化得更好未针对频繁增删进行特殊优化
默认原型无有,可能意外地从原型链上继承到键名

结论

JavaScript 的 Map 是一种高级抽象,其底层核心是哈希表,并辅以链表等数据结构来处理冲突和维护顺序。正是这种实现使得它具备了快速查找和有序迭代的强大特性。

Map Set 映射和集合
Map

是一个带键的数据项的集合,就像一个 Object 一样。但是它们最大的差别是 Map 允许任何类型的键(key)。 它的方法和属性如下:

  • new Map([[键,值],[键,值]]) —— 创建 map。
  • map.set(key, value) —— 根据键存储值。
  • map.get(key) —— 根据键来返回值,如果 map 中不存在对应的 key,则返回 undefined。
  • map.has(key) —— 如果 key 存在则返回 true,否则返回 false。
  • map.delete(key) —— 删除指定键的值。
  • map.clear() —— 清空 map。
  • map.size —— 返回当前元素个数。 每一次 map.set 调用都会返回 map 本身,所以我们可以进行“链式”调用

如果要在 map 里使用循环,可以使用以下三个方法:

  • map.keys() —— 遍历并返回一个包含所有键的可迭代对象,
  • map.values() —— 遍历并返回一个包含所有值的可迭代对象,
  • map.entries() —— 遍历并返回一个包含所有实体 [key, value] 的可迭代对象,for..of 在默认情况下使用的就是这个。

从对象创建Map:

let obj = {
    name: "dano",
    age: 21,
};

let map = new Map(Object.entries(obj));

console.log(map);//Map(2) { 'name' => 'dano', 'age' => 21 }

从Map创建对象:

let prices = Object.fromEntries([
    ['shitr', 2],
    ['apple', 23],
    ['fruit',43]
])

console.log(prices);//{ shitr: 2, apple: 23, fruit: 43 }
Set

Set 是一个特殊的类型集合 —— “值的集合”(没有键),它的每一个值只能出现一次。 它的主要方法如下:

  • new Set(iterable) —— 创建一个 set,如果提供了一个 iterable 对象(通常是数组),将会从数组里面复制值到 set 中。
  • set.add(value) —— 添加一个值,返回 set 本身
  • set.delete(value) —— 删除值,如果 value 在这个方法调用的时候存在则返回 true ,否则返回 false。
  • set.has(value) —— 如果 value 在 set 中,返回 true,否则返回 false。
  • set.clear() —— 清空 set。
  • set.size —— 返回元素个数。

我们有客人来访,我们想记住他们每一个人。但是已经来访过的客人再次来访,不应造成重复记录。每个访客必须只被“计数”一次。

let set = new Set();

set.add({ name: 'dano' }).add({ name: 'jungle' });

console.log(set.size);//2

for (let value of set) {
    console.log(value.name);
    //dano jungle
}

注意一件有趣的事儿。forEach 的回调函数有三个参数:一个 value,然后是 同一个值 valueAgain,最后是目标对象。没错,同一个值在参数里出现了两次。

forEach 的回调函数有三个参数,是为了与 Map 兼容。当然,这看起来确实有些奇怪。但是这对在特定情况下轻松地用 Set 代替 Map 很有帮助,反之亦然。

Map 中用于迭代的方法在 Set 中也同样支持:

  • set.keys() —— 遍历并返回一个包含所有值的可迭代对象,
  • set.values() —— 与 set.keys() 作用相同,这是为了兼容 Map,
  • set.entries() —— 遍历并返回一个包含所有的实体 [value, value] 的可迭代对象,它的存在也是为了兼容 Map。
弱映射和弱集合

8/11/26 【修正】原内容:WeakMap 的键必须是对象;修改点:当前规范还允许未注册的 Symbol,其他原始值仍不可用。WeakMap 的键可以是对象或未注册的 Symbol。 如果我们在 weakMap 中使用一个对象作为键,并且没有其他对这个对象的引用 —— 该对象将会被从内存(和map)中自动清除。也就是说WeakMap并不能阻止对象被回收,而普通的Map可以。

let obj = { name: 'dano' };

//let map = new Map().set(obj, "shit");
let weakmap = new WeakMap().set(obj, "shit2");

obj = null;

//console.log(map.get(obj));
console.log(weakmap.get(obj));

WeakMap 的主要应用场景是 额外数据的存储。

假如我们正在处理一个“属于”另一个代码的一个对象,也可能是第三方库,并想存储一些与之相关的数据,那么这些数据就应该与这个对象共存亡 —— 这时候 WeakMap 正是我们所需要的利器。

我们将这些数据放到 WeakMap 中,并使用该对象作为这些数据的键,那么当该对象被垃圾回收机制回收后,这些数据也会被自动清除。

WeakSet 的表现类似: 8/11/26 【修正】原内容:WeakSet 只能添加对象;修改点:当前规范还允许未注册的 Symbol。

  • 与 Set 类似,但只能添加对象或未注册的 Symbol。
  • 对象只有在其它某个(些)地方能被访问的时候,才能留在 WeakSet 中。
  • 跟 Set 一样,WeakSet 支持 add,has 和 delete 方法,但不支持 size 和 keys(),并且不可迭代。

WeakSet 是类似于 Set 的集合,可弱持有对象或未注册的 Symbol;当对象键通过其他方式不可达时,垃圾回收器可以回收它。

在 JavaScript 中,Set 是 ES6 引入的一种集合数据结构,用于存储唯一的值(不允许重复)。它类似于数组,但成员的值都是唯一的,没有重复项。Set 本身是一个构造函数,用于创建 Set 实例。

Set 的基本特性
  1. 值的唯一性:Set 中的每个值都是唯一的,无论添加多少次相同的值,最终只会保留一个。 (判断唯一性的方式类似 ===,但特殊地,NaN 被视为与自身相等,这与 === 不同)

8/11/26 【修正】原内容:Set 无序;修改点:Set 没有数字下标,但迭代遵循插入顺序。 2. 插入顺序:Set 没有数组式数字索引,但会按插入顺序迭代。

  1. 可迭代性:Set 是可迭代对象,可通过 for...of 循环遍历,也支持扩展运算符(...)。
Set 的常用方法与属性
基本操作
  • new Set([iterable]):创建 Set 实例,可选参数为可迭代对象(如数组),会自动去重。
  • add(value):添加值,返回 Set 本身(可链式调用)。
  • delete(value):删除值,返回布尔值(是否删除成功)。
  • has(value):判断是否包含某个值,返回布尔值。
  • clear():清空所有元素,无返回值。
  • size:属性,返回元素个数(类似数组的 length)。
示例:
// 创建 Set(自动去重)
const set = new Set([1, 2, 2, 3, 3, 3]);
console.log(set); // Set(3) {1, 2, 3}
console.log(set.size); // 3

// 添加元素
set.add(4).add(5); // 链式调用
console.log(set); // Set(5) {1, 2, 3, 4, 5}

// 判断是否包含
console.log(set.has(3)); // true
console.log(set.has(6)); // false

// 删除元素
set.delete(2);
console.log(set); // Set(4) {1, 3, 4, 5}

// 清空
set.clear();
console.log(set.size); // 0
遍历方法

Set 提供了多种遍历方式,由于元素无序,遍历顺序与插入顺序一致:

  • keys():返回键名的迭代器(Set 的键名与键值相同)。
  • values():返回键值的迭代器(常用)。
  • entries():返回 [value, value] 的迭代器(因为键名与键值相同)。
  • forEach(callback):使用回调函数遍历每个元素。
示例:
const set = new Set(['a', 'b', 'c']);

// for...of 遍历 values()
for (const value of set.values()) {
  console.log(value); // 'a' 'b' 'c'
}

// forEach 遍历
set.forEach((value) => {
  console.log(value); // 'a' 'b' 'c'
});

// 扩展运算符转为数组
const arr = [...set];
console.log(arr); // ['a', 'b', 'c']
Set 的典型应用场景
  1. 数组去重:利用 Set 的唯一性快速去重。
   const arr = [1, 2, 2, 3, 3, 3];
   const uniqueArr = [...new Set(arr)]; // [1, 2, 3]
  1. 判断元素是否存在:has 方法的性能优于数组的 indexOf 或 includes(尤其数据量大时)。

  2. 存储不重复的集合数据:如用户ID、标签等需要唯一标识的数据。

  3. 实现交集、并集、差集:

   const setA = new Set([1, 2, 3]);
   const setB = new Set([2, 3, 4]);

   // 并集
   const union = new Set([...setA, ...setB]); // {1, 2, 3, 4}

   // 交集
   const intersection = new Set([...setA].filter(x => setB.has(x))); // {2, 3}

   // 差集(setA 有而 setB 没有)
   const difference = new Set([...setA].filter(x => !setB.has(x))); // {1}
与数组的区别
特性Set数组(Array)
元素唯一性自动去重,无重复值允许重复值
索引访问无索引,无法通过下标访问有索引,可通过 [index] 访问
常用操作增删查效率高(哈希表实现)增删查效率受位置影响
适用场景存储唯一值、集合运算有序数据、需要索引访问
总结

Set 是一种高效的集合数据结构,核心优势是自动去重和快速的增删查操作。它弥补了传统数组在处理“唯一值集合”时的不足,常用于数组去重、集合运算等场景。在需要存储不重复数据且无需索引的场景中,Set 是比数组更合适的选择。

廖雪峰教程 · 3快速入门/3.8 Map与Set

Map与Set

JavaScript的默认对象表示方式{}可以视为其他语言中的Map或Dictionary的数据结构,即一组键值对。

但是JavaScript的对象有个小问题,就是键必须是字符串。但实际上Number或者其他数据类型作为键也是非常合理的。

Map

Map是一组键值对的结构,具有极快的查找速度。

举个例子,假设要根据同学的名字查找对应的成绩,如果用Array实现,需要两个Array:

let names = ['Michael', 'Bob', 'Tracy'];
let scores = [95, 75, 85];

给定一个名字,要查找对应的成绩,就先要在names中找到对应的位置,再从scores取出对应的成绩,Array越长,耗时越长。

如果用Map实现,只需要一个“名字”-“成绩”的对照表,直接根据名字查找成绩,无论这个表有多大,查找速度都不会变慢。用JavaScript写一个Map如下:

let m = new Map([['Michael', 95], ['Bob', 75], ['Tracy', 85]]);
m.get('Michael'); // 95

初始化Map需要一个二维数组,或者直接初始化一个空Map。Map具有以下方法:

let m = new Map(); // 空Map
m.set('Adam', 67); // 添加新的key-value
m.set('Bob', 59);
m.has('Adam'); // 是否存在key 'Adam': true
m.get('Adam'); // 67
m.delete('Adam'); // 删除key 'Adam'
m.get('Adam'); // undefined

由于一个key只能对应一个value,所以,多次对一个key放入value,后面的值会把前面的值冲掉:

let m = new Map();
m.set('Adam', 67);
m.set('Adam', 88);
m.get('Adam'); // 88
Set

Set和Map类似,也是一组key的集合,但不存储value。由于key不能重复,所以,在Set中,没有重复的key。

要创建一个Set,需要提供一个Array作为输入,或者直接创建一个空Set:

let s1 = new Set(); // 空Set
let s2 = new Set([1, 2, 3]); // 含1, 2, 3

重复元素在Set中自动被过滤:

let s = new Set([1, 2, 3, 3, '3']);
s; // Set {1, 2, 3, "3"}

通过delete(key)方法可以删除元素:

let s = new Set([1, 2, 3]);
s; // Set {1, 2, 3}
s.delete(3);
s; // Set {1, 2}

廖雪峰教程 · 3快速入门/3.9 iterable

iterable

遍历Array可以采用下标循环,遍历Map和Set就无法使用下标。为了统一集合类型,ES6标准引入了新的iterable类型,Array、Map和Set都属于iterable类型。

具有iterable类型的集合可以通过新的for ... of循环来遍历。用法如下:

let a = ['A', 'B', 'C'];
let s = new Set(['A', 'B', 'C']);
let m = new Map([[1, 'x'], [2, 'y'], [3, 'z']]);
for (let x of a) { // 遍历Array
    console.log(x);
}
for (let x of s) { // 遍历Set
    console.log(x);
}
for (let x of m) { // 遍历Map
    console.log(x[0] + '=' + x[1]);
}

你可能会有疑问,for ... of循环和for ... in循环有何区别?

for ... in循环由于历史遗留问题,它遍历的实际上是对象的属性名称。一个Array数组实际上也是一个对象,它的每个元素的索引被视为一个属性。

当我们手动给Array对象添加了额外的属性后,for ... in循环将带来意想不到的意外效果:

let a = ['A', 'B', 'C'];
a.name = 'Hello';
for (let x in a) {
    console.log(x); // '0', '1', '2', 'name'
}

for ... in循环将把name包括在内,但Array的length属性却不包括在内。

for ... of循环则完全修复了这些问题,它只循环集合本身的元素:

let a = ['A', 'B', 'C'];
a.name = 'Hello';
for (let x of a) {
    console.log(x); // 'A', 'B', 'C'
}

这就是为什么要引入新的for ... of循环。

然而,更好的方式是直接使用iterable内置的forEach方法,它接收一个函数,每次迭代就自动回调该函数。

注意,forEach()方法是ES5.1标准引入的,你需要测试浏览器是否支持。

Set与Array类似,但Set没有索引,因此回调函数的前两个参数都是元素本身:

let s = new Set(['A', 'B', 'C']);
s.forEach(function (element, sameElement, set) {
    console.log(element);
});

Map的回调函数参数依次为value、key和map本身:

let m = new Map([[1, 'x'], [2, 'y'], [3, 'z']]);
m.forEach(function (value, key, map) {
    console.log(value);
});

如果对某些参数不感兴趣,由于JavaScript的函数调用不要求参数必须一致,因此可以忽略它们。例如,只需要获得Array的element:

let a = ['A', 'B', 'C'];
a.forEach(function (element) {
    console.log(element);
});